Skip to content

Conversation

@TaduJR
Copy link
Contributor

@TaduJR TaduJR commented Jul 25, 2025

Fixes #716

Details

This PR fixes a bug where MarkdownTextInput with multiline={false} incorrectly inserts spaces when users select all text and type a replacement character. The issue was caused by the library treating all text input as potentially multiline during processing, regardless of the multiline prop value.

Root Cause:
The library was adding BR elements (containing \n characters) and newlines during both DOM generation and text extraction phases, even for single-line inputs. These newlines then got converted to spaces during text normalization.

Changes Made:

1. Fixed BR element addition in addTextToElement() function (parserUtils.ts)

  • Made BR element addition conditional on isMultiline parameter
  • Prevents unwanted newline characters in single-line inputs

2. Updated addTextToElement() function calls (parserUtils.ts)

  • Pass isMultiline parameter to all function calls within parseRangesToHTMLNodes()
  • Ensures multiline setting is respected throughout DOM generation

3. Enhanced parseInnerHTMLToText() function (inputUtils.ts)

  • Added isMultiline = true parameter to maintain backward compatibility
  • Made newline addition conditional in both top-level component processing and BR element parsing
  • Prevents unwanted newlines during text extraction for single-line inputs

4. Connected multiline prop in MarkdownTextInput.web.tsx

  • Pass multiline prop to parseInnerHTMLToText() function
  • Ensures the component's multiline setting flows through the entire processing chain

Impact:

  • ✅ Single-line inputs no longer get unwanted spaces
  • ✅ Multiline inputs continue to work correctly
  • ✅ Markdown highlighting preserved for both input types
  • ✅ No breaking changes to existing API
  • ✅ Backward compatible (all new parameters have defaults)

Related Issues

GH_LINK

Manual Tests

Test Scenarios Covered:

  1. Single-line text replacement:

    • Create MarkdownTextInput with multiline={false}
    • Enter text, select all, type replacement
    • Verify no extra spaces are added
  2. Multiline preservation:

    • Create MarkdownTextInput with multiline={true}
    • Test that line breaks continue to work correctly
  3. Markdown highlighting:

    • Test single-line inputs with various markdown syntax
    • Verify highlighting works without adding unwanted spaces
  4. Edge cases:

    • Empty inputs, inputs with only spaces, special characters
    • Mixed markdown content (bold, italic, etc.)
  5. Regression testing:

    • Verified existing multiline functionality remains unchanged
    • Confirmed no impact on markdown parsing or rendering

Manual Testing Steps:

  1. Open WebExample or any app using the library
  2. Create a search input with multiline={false} and type="markdown"
  3. Enter text like "type:expense group-by:reports"
  4. Select all text (Ctrl+A)
  5. Type a single character like "t"
  6. Before fix: Shows "t " (with unwanted space)
  7. After fix: Shows "t" (clean, no extra space)

@github-actions
Copy link

github-actions bot commented Jul 25, 2025

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@TaduJR
Copy link
Contributor Author

TaduJR commented Jul 25, 2025

Fix Summary

What changes do you think we should make in order to solve the problem?

We should make newline addition conditional on the isMultiline parameter throughout the library. The following changes are needed:

  1. Fix BR element addition in addTextToElement() function (parserUtils.ts)

Current problematic code:

if (index < lines.length - 1 || (index === 0 && line === '')) {
  addBrElement(node);
}

Why change is needed: This code always adds BR elements (containing \n characters) regardless of whether the input should be single-line or multiline. For
single-line inputs, these BR elements create unwanted newlines that get converted to spaces.

Required change:

// Only add BR elements for multiline inputs or when there are actual line breaks
if (isMultiline && (index < lines.length - 1 || (index === 0 && line === ''))) {
  addBrElement(node);
}
  1. Pass isMultiline parameter to addTextToElement() calls (parserUtils.ts)

Current problematic code:

// Line 232
addTextToElement(currentParentNode, line.text);

// Line 258
addTextToElement(currentParentNode, textBeforeRange);

// Line 288
addTextToElement(currentParentNode, textAfterRange);

Why change is needed: These function calls don't pass the isMultiline parameter, causing them to default to true and always add BR elements even for single-line
inputs.

Required changes:

// Line 232
addTextToElement(currentParentNode, line.text, isMultiline);

// Line 258
addTextToElement(currentParentNode, textBeforeRange, isMultiline);

// Line 288
addTextToElement(currentParentNode, textAfterRange, isMultiline);
  1. Add isMultiline parameter to parseInnerHTMLToText() function (inputUtils.ts)

Current function signature:

function parseInnerHTMLToText(target: MarkdownTextInputElement, cursorPosition: number, inputType?: string): string

Why change is needed: The function doesn't know whether the input should be single-line or multiline when extracting text from DOM, so it always assumes multiline
behavior.

Required change:

function parseInnerHTMLToText(target: MarkdownTextInputElement, cursorPosition: number, inputType?: string, isMultiline = true): string
  1. Fix newline addition in top-level component processing (inputUtils.ts)

Current problematic code:

if (shouldAddNewline && !containsEmptyBlockElement) {
  text += '\n';
  shouldAddNewline = false;
}
shouldAddNewline = true;

Why change is needed: This code always adds newlines between top-level components, even for single-line inputs where newlines should not be added.

Required change:

// Only add newlines for multiline inputs
if (isMultiline && shouldAddNewline && !containsEmptyBlockElement) {
  text += '\n';
  shouldAddNewline = false;
}
shouldAddNewline = isMultiline;
  1. Fix BR element parsing (inputUtils.ts)

Current problematic code:

if (parentNode && parentNode.parentElement?.contentEditable !== 'true' && !!node.getAttribute('data-id')) {
  // Parse br elements into newlines only if their parent is not a child of the MarkdownTextInputElement
  text += '\n';
}

Why change is needed: This code converts BR elements to newlines regardless of whether the input should be single-line, causing unwanted newlines in the extracted
text.

Required change:

if (isMultiline && parentNode && parentNode.parentElement?.contentEditable !== 'true' && !!node.getAttribute('data-id')) {
  // Parse br elements into newlines only if their parent is not a child of the MarkdownTextInputElement - and now only for multiline inputs
  text += '\n';
}
  1. Pass multiline prop to parseInnerHTMLToText() (MarkdownTextInput.web.tsx)

Current problematic code:

let parsedText = normalizeValue(
  inputType === 'pasteText' ? pasteContent.current || '' : parseInnerHTMLToText(e.target as MarkdownTextInputElement, contentSelection.current.start, inputType),
);

Why change is needed: The function call doesn't pass the multiline prop, so parseInnerHTMLToText() defaults to multiline behavior even for single-line inputs.

Required change:

let parsedText = normalizeValue(
  inputType === 'pasteText' ? pasteContent.current || '' : parseInnerHTMLToText(e.target as MarkdownTextInputElement, contentSelection.current.start, inputType,
multiline),
);

@tomekzaw
Copy link
Collaborator

cc @jmusial Will you take a look?

Copy link
Collaborator

@jmusial jmusial left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to me that minimal fix requires only 3, 4 and 6.

2 seems reasonable addition. Would you mind adding test cases for that as well ?

Also please add before & after videos showcasing the changes and test the solution with E/App

const containsEmptyBlockElement = firstChild?.getAttribute?.('data-type') === 'block' && firstChild.textContent === '';
if (shouldAddNewline && !containsEmptyBlockElement) {
// Only add newlines for multiline inputs
if (isMultiline && shouldAddNewline && !containsEmptyBlockElement) {
Copy link
Collaborator

@jmusial jmusial Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we rather

      if (!isMultiline && !inputType && node.nodeType === Node.TEXT_NODE) {

in line #70 ?

Copy link
Collaborator

@jmusial jmusial Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we discuss this change here, rather than adding in the comments ? It helps keeping the context :)

Your'e correct , there is a clerical error in my proposed solution, should be

      if (!isMultiline || (!inputType && node.nodeType === Node.TEXT_NODE)) {

should handle all the cases.

@TaduJR
Copy link
Contributor Author

TaduJR commented Jul 26, 2025

Seems to me that minimal fix requires only 3, 4 and 6.

2 seems reasonable addition. Would you mind adding test cases for that as well ?

Also please add before & after videos showcasing the changes and test the solution with E/App

Hello @jmusial

Before: image
After: https://www.loom.com/share/18e42c66bfbd4bd39f16237c039671c1

Since the before is similar to Expensify/App#64831 issue screenshot, I just referenced it.

@TaduJR
Copy link
Contributor Author

TaduJR commented Jul 26, 2025

Seems to me that minimal fix requires only 3, 4 and 6.

2 seems reasonable addition. Would you mind adding test cases for that as well ?

Also please add before & after videos showcasing the changes and test the solution with E/App

Seems to me that minimal fix requires only 3, 4 and 6.

2 seems reasonable addition. Would you mind adding test cases for that as well ?

Also please add before & after videos showcasing the changes and test the solution with E/App

Thank you for the feedback! You're absolutely right that changes 3, 4, and 6 form the core of the fix. However, I'd like to respectfully suggest that change 1 is also critical for a complete solution.

Why Change 1 (BR element fix) is necessary:

The issue occurs in two places where newlines are introduced:

  1. DOM generation (addTextToElement - change 1)
  2. Text extraction (parseInnerHTMLToText - changes 3,4,6)

Without change 1, single-line inputs will still have BR elements inserted into the DOM, which can cause issues in other scenarios beyond just the text extraction phase.

@TaduJR
Copy link
Contributor Author

TaduJR commented Jul 26, 2025

Also Here comprehensive test cases covering:

  1. Single-line text replacement test:
    it('should not add unwanted spaces when replacing selected text in single-line input', () => {
    // Test multiline={false} with text selection and replacement
    });
  2. Multiline preservation test:
    it('should preserve line breaks in multiline inputs', () => {
    // Test multiline={true} continues to work correctly
    });
  3. BR element generation test:
    it('should not generate BR elements for single-line inputs', () => {
    // Test DOM structure doesn't include unnecessary BR elements
    });
  4. Text extraction test:
    it('should extract clean text from single-line inputs without newlines', () => {
    // Test parseInnerHTMLToText with isMultiline=false
    });

@jmusial
Copy link
Collaborator

jmusial commented Jul 28, 2025

Also Here comprehensive test cases covering:

  1. Single-line text replacement test:
    it('should not add unwanted spaces when replacing selected text in single-line input', () => {
    // Test multiline={false} with text selection and replacement
    });
  2. Multiline preservation test:
    it('should preserve line breaks in multiline inputs', () => {
    // Test multiline={true} continues to work correctly
    });
  3. BR element generation test:
    it('should not generate BR elements for single-line inputs', () => {
    // Test DOM structure doesn't include unnecessary BR elements
    });
  4. Text extraction test:
    it('should extract clean text from single-line inputs without newlines', () => {
    // Test parseInnerHTMLToText with isMultiline=false
    });

yup, test scenarios look good, please implement

@TaduJR TaduJR force-pushed the fix-unwanted-space-insertion-in-single-line-MarkdownTextInput-when-replacing-selected-text branch from 387eea1 to 3bc2e59 Compare July 28, 2025 14:33
@TaduJR TaduJR requested a review from jmusial July 28, 2025 14:35
Copy link
Collaborator

@jmusial jmusial left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@tomekzaw do you want to take a look as well ?

@jmusial
Copy link
Collaborator

jmusial commented Jul 30, 2025

@TaduJR I think you need to still sign the CLA

@TaduJR
Copy link
Contributor Author

TaduJR commented Jul 30, 2025

I have read the CLA Document and I hereby sign the CLA

CLABotify added a commit to Expensify/CLA that referenced this pull request Jul 30, 2025
@TaduJR
Copy link
Contributor Author

TaduJR commented Jul 30, 2025

Just signed it @jmusial

Copy link
Collaborator

@tomekzaw tomekzaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for the PR!

Copy link

@allroundexperts allroundexperts left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

@tomekzaw
Copy link
Collaborator

@TaduJR I can't merge the PR because of the following error:

Screenshot 2025-07-30 at 14 51 56

Please sign your commits retroactively (or submit another PR if that's easier).

@TaduJR TaduJR force-pushed the fix-unwanted-space-insertion-in-single-line-MarkdownTextInput-when-replacing-selected-text branch from 3bc2e59 to 1f16547 Compare July 30, 2025 18:56
@TaduJR TaduJR force-pushed the fix-unwanted-space-insertion-in-single-line-MarkdownTextInput-when-replacing-selected-text branch from 1f16547 to 722627e Compare July 31, 2025 06:07
@TaduJR TaduJR closed this Jul 31, 2025
@TaduJR TaduJR deleted the fix-unwanted-space-insertion-in-single-line-MarkdownTextInput-when-replacing-selected-text branch July 31, 2025 06:10
@TaduJR TaduJR restored the fix-unwanted-space-insertion-in-single-line-MarkdownTextInput-when-replacing-selected-text branch July 31, 2025 06:11
@TaduJR
Copy link
Contributor Author

TaduJR commented Jul 31, 2025

Hello @tomekzaw

Here is the fully signed PR #718

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Unwanted space insertion in single-line MarkdownTextInput when replacing selected text

4 participants